home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2005 October / PCWOCT05.iso / Software / FromTheMag / XAMPP 1.4.14 / xampp-win32-1.4.14-installer.exe / xampp / php / pear / Services / Weather / Metar.php < prev    next >
Encoding:
PHP Script  |  2004-10-01  |  76.2 KB  |  1,624 lines

  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4: */
  3. // +----------------------------------------------------------------------+
  4. // | PHP version 4                                                        |
  5. // +----------------------------------------------------------------------+
  6. // | Copyright (c) 1997-2004 The PHP Group                                |
  7. // +----------------------------------------------------------------------+
  8. // | This source file is subject to version 2.0 of the PHP license,       |
  9. // | that is bundled with this package in the file LICENSE, and is        |
  10. // | available through the world-wide-web at                              |
  11. // | http://www.php.net/license/2_02.txt.                                 |
  12. // | If you did not receive a copy of the PHP license and are unable to   |
  13. // | obtain it through the world-wide-web, please send a note to          |
  14. // | license@php.net so we can mail you a copy immediately.               |
  15. // +----------------------------------------------------------------------+
  16. // | Authors: Alexander Wirtz <alex@pc4p.net>                             |
  17. // +----------------------------------------------------------------------+
  18. //
  19. // $Id: Metar.php,v 1.65 2004/07/11 21:45:41 eru Exp $
  20.  
  21. /**
  22. * @package      Services_Weather
  23. * @filesource
  24. */
  25.  
  26. /**
  27. */
  28. require_once "Services/Weather/Common.php";
  29.  
  30. require_once "DB.php";
  31.  
  32. // {{{ class Services_Weather_Metar
  33. /**
  34. * PEAR::Services_Weather_Metar
  35. *
  36. * This class acts as an interface to the METAR/TAF service of
  37. * weather.noaa.gov. It searches for locations given in ICAO notation and
  38. * retrieves the current weather data.
  39. *
  40. * Of course the parsing of the METAR-data has its limitations, as it
  41. * follows the Federal Meteorological Handbook No.1 with modifications to
  42. * accomodate for non-US reports, so if the report deviates from these
  43. * standards, you won't get it parsed correctly.
  44. * Anything that is not parsed, is saved in the "noparse" array-entry,
  45. * returned by getWeather(), so you can do your own parsing afterwards. This
  46. * limitation is specifically given for remarks, as the class is not
  47. * processing everything mentioned there, but you will get the most common
  48. * fields like precipitation and temperature-changes. Again, everything not
  49. * parsed, goes into "noparse".
  50. *
  51. * If you think, some important field is missing or not correctly parsed,
  52. * please file a feature-request/bugreport at http://pear.php.net/ and be
  53. * sure to provide the METAR (or TAF) report with a _detailed_ explanation!
  54. *
  55. * For working examples, please take a look at
  56. *     docs/Services_Weather/examples/metar-basic.php
  57. *     docs/Services_Weather/examples/metar-extensive.php
  58. *
  59. * @author       Alexander Wirtz <alex@pc4p.net>
  60. * @link         http://weather.noaa.gov/weather/metar.shtml
  61. * @link         http://weather.noaa.gov/weather/taf.shtml
  62. * @example      examples/metar-basic.php        metar-basic.php
  63. * @example      examples/metar-extensive.php    metar-extensive.php
  64. * @package      Services_Weather
  65. * @license      http://www.php.net/license/2_02.txt
  66. * @version      1.3
  67. */
  68. class Services_Weather_Metar extends Services_Weather_Common
  69. {
  70.     // {{{ properties
  71.     /**
  72.     * Information to access the location DB
  73.     *
  74.     * @var      object  DB                  $_db
  75.     * @access   private
  76.     */
  77.     var $_db;
  78.     
  79.     /**
  80.     * The source METAR uses
  81.     *
  82.     * @var      string                      $_sourceMetar
  83.     * @access   private
  84.     */
  85.     var $_sourceMetar;
  86.  
  87.     /**
  88.     * The source TAF uses
  89.     *
  90.     * @var      string                      $_sourceTaf
  91.     * @access   private
  92.     */
  93.     var $_sourceTaf;
  94.  
  95.     /**
  96.     * This path is used to find the METAR data
  97.     *
  98.     * @var      string                      $_sourcePathMetar
  99.     * @access   private
  100.     */
  101.     var $_sourcePathMetar;
  102.  
  103.     /**
  104.     * This path is used to find the TAF data
  105.     *
  106.     * @var      string                      $_sourcePathTaf
  107.     * @access   private
  108.     */
  109.     var $_sourcePathTaf;
  110.     // }}}
  111.  
  112.     // {{{ constructor
  113.     /**
  114.     * Constructor
  115.     *
  116.     * @param    array                       $options
  117.     * @param    mixed                       $error
  118.     * @throws   PEAR_Error
  119.     * @see      Science_Weather::Science_Weather
  120.     * @access   private
  121.     */
  122.     function Services_Weather_Metar($options, &$error)
  123.     {
  124.         $perror = null;
  125.         $this->Services_Weather_Common($options, $perror);
  126.         if (Services_Weather::isError($perror)) {
  127.             $error = $perror;
  128.             return;
  129.         }
  130.         
  131.         // Set options accordingly        
  132.         if (isset($options["dsn"])) {
  133.             if (isset($options["dbOptions"])) {
  134.                 $status = $this->setMetarDB($options["dsn"], $options["dbOptions"]);
  135.             } else {
  136.                 $status = $this->setMetarDB($options["dsn"]);
  137.             }
  138.         }
  139.         if (Services_Weather::isError($status)) {
  140.             $error = $status;
  141.             return;
  142.         }
  143.         
  144.         // Setting the data sources for METAR and TAF - have to watch out for older API usage
  145.         if (($source = isset($options["source"])) || isset($options["sourceMetar"])) {
  146.             $sourceMetar = $source ? $options["source"] : $options["sourceMetar"]; 
  147.             if (($sourcePath = isset($options["sourcePath"])) || isset($options["sourcePathMetar"])) {
  148.                 $sourcePathMetar = $sourcePath ? $options["sourcePath"] : $options["sourcePathMetar"];
  149.             } else {
  150.                 $sourcePathMetar = "";
  151.             }
  152.         } else {
  153.             $sourceMetar = "http";
  154.             $sourcePathMetar = "";
  155.         }
  156.         if (isset($options["sourceTaf"])) {
  157.             $sourceTaf = $options["sourceTaf"];
  158.             if (isset($option["sourcePathTaf"])) {
  159.                 $sourcePathTaf = $options["sourcePathTaf"];
  160.             } else {
  161.                 $soucePathTaf = "";
  162.             }
  163.         } else {
  164.             $sourceTaf = "http";
  165.             $sourcePathTaf = "";
  166.         }
  167.         $this->setMetarSource($sourceMetar, $sourcePathMetar, $sourceTaf, $sourcePathTaf);
  168.     }
  169.     // }}}
  170.  
  171.     // {{{ setMetarDB()
  172.     /**
  173.     * Sets the parameters needed for connecting to the DB, where the
  174.     * location-search is fetching its data from. You need to build a DB
  175.     * with the external tool buildMetarDB first, it fetches the locations
  176.     * and airports from a NOAA-website.
  177.     *
  178.     * @param    string                      $dsn
  179.     * @param    array                       $dbOptions
  180.     * @return   DB_Error|bool
  181.     * @throws   DB_Error
  182.     * @see      DB::parseDSN
  183.     * @access   public
  184.     */
  185.     function setMetarDB($dsn, $dbOptions = array())
  186.     {
  187.         $dsninfo = DB::parseDSN($dsn);
  188.         if (is_array($dsninfo) && !isset($dsninfo["mode"])) {
  189.             $dsninfo["mode"]= 0644;
  190.         }
  191.         
  192.         // Initialize connection to DB and store in object if successful
  193.         $db =  DB::connect($dsninfo, $dbOptions);
  194.         if (DB::isError($db)) {
  195.             return $db;
  196.         }
  197.         $this->_db = $db;
  198.  
  199.         return true;
  200.     }
  201.     // }}}
  202.  
  203.     // {{{ setMetarSource()
  204.     /**
  205.     * Sets the source, where the class tries to locate the METAR/TAF data
  206.     *
  207.     * Source can be http, ftp or file.
  208.     * Alternate sourcepaths can be provided.
  209.     *
  210.     * @param    string                      $sourceMetar
  211.     * @param    string                      $sourcePathMetar
  212.     * @param    string                      $sourceTaf
  213.     * @param    string                      $sourcePathTaf
  214.     * @access   public
  215.     */
  216.     function setMetarSource($sourceMetar, $sourcePathMetar = "", $sourceTaf = "", $sourcePathTaf = "")
  217.     {
  218.         if (in_array($sourceMetar, array("http", "ftp", "file"))) {
  219.             $this->_sourceMetar = $sourceMetar;
  220.         }
  221.         if (strlen($sourcePathMetar)) {
  222.             $this->_sourcePathMetar = $sourcePathMetar;
  223.         } else {
  224.             switch ($sourceMetar) {
  225.                 case "http":
  226.                     $this->_sourcePathMetar = "http://weather.noaa.gov/pub/data/observations/metar/stations/";
  227.                     break;
  228.                 case "ftp":
  229.                     $this->_sourcePathMetar = "ftp://weather.noaa.gov/data/observations/metar/stations/";
  230.                     break;
  231.                 case "file":
  232.                     $this->_sourcePathMetar = "./";
  233.                     break;
  234.             }
  235.         }
  236.         if (in_array($sourceTaf, array("http", "ftp", "file"))) {
  237.             $this->_sourceTaf = $sourceTaf;
  238.         }
  239.         if (strlen($sourcePathTaf)) {
  240.             $this->_sourcePathTaf = $sourcePathTaf;
  241.         } else {
  242.             switch ($sourceTaf) {
  243.                 case "http":
  244.                     $this->_sourcePathTaf = "http://weather.noaa.gov/pub/data/forecasts/taf/stations/";
  245.                     break;
  246.                 case "ftp":
  247.                     $this->_sourcePathTaf = "ftp://weather.noaa.gov/data/forecasts/taf/stations/";
  248.                     break;
  249.                 case "file":
  250.                     $this->_sourcePathTaf = "./";
  251.                     break;
  252.             }
  253.         }
  254.     }
  255.     // }}}
  256.  
  257.     // {{{ _checkLocationID()
  258.     /**
  259.     * Checks the id for valid values and thus prevents silly requests to
  260.     * METAR server
  261.     *
  262.     * @param    string                      $id
  263.     * @return   PEAR_Error|bool
  264.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_NO_LOCATION
  265.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  266.     * @access   private
  267.     */
  268.     function _checkLocationID($id)
  269.     {
  270.         if (is_array($id) || is_object($id) || !strlen($id)) {
  271.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_NO_LOCATION, __FILE__, __LINE__);
  272.         } elseif (!ctype_alpha($id) || (strlen($id) > 4)) {
  273.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__);
  274.         }
  275.  
  276.         return true;
  277.     }
  278.     // }}}
  279.  
  280.     // {{{ _parseWeatherData()
  281.     /**
  282.     * Parses the data returned by the provided source and caches it
  283.     *    
  284.     * METAR KPIT 091955Z COR 22015G25KT 3/4SM R28L/2600FT TSRA OVC010CB
  285.     * 18/16 A2992 RMK SLP045 T01820159
  286.     *
  287.     * @param    string                      $source
  288.     * @return   PEAR_Error|array
  289.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  290.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  291.     * @access   private
  292.     */
  293.     function _parseWeatherData($source)
  294.     {
  295.         static $compass;
  296.         static $clouds;
  297.         static $conditions;
  298.         static $sensors;
  299.         if (!isset($compass)) {
  300.             $compass = array(
  301.                 "N", "NNE", "NE", "ENE",
  302.                 "E", "ESE", "SE", "SSE",
  303.                 "S", "SSW", "SW", "WSW",
  304.                 "W", "WNW", "NW", "NNW"
  305.             );
  306.             $clouds    = array(
  307.                 "skc"         => "sky clear",
  308.                 "nsc"         => "no significant cloud",
  309.                 "few"         => "few",
  310.                 "sct"         => "scattered",
  311.                 "bkn"         => "broken",
  312.                 "ovc"         => "overcast",
  313.                 "vv"          => "vertical visibility",
  314.                 "tcu"         => "Towering Cumulus",
  315.                 "cb"          => "Cumulonimbus",
  316.                 "clr"         => "clear below 12,000 ft"
  317.             );
  318.             $conditions = array(
  319.                 "+"           => "heavy",        "-"           => "light",
  320.  
  321.                 "vc"          => "vicinity",     "re"          => "recent",
  322.                 "nsw"         => "no significant weather",
  323.  
  324.                 "mi"          => "shallow",      "bc"          => "patches",
  325.                 "pr"          => "partial",      "ts"          => "thunderstorm",
  326.                 "bl"          => "blowing",      "sh"          => "showers",
  327.                 "dr"          => "low drifting", "fz"          => "freezing",
  328.  
  329.                 "dz"          => "drizzle",      "ra"          => "rain",
  330.                 "sn"          => "snow",         "sg"          => "snow grains",
  331.                 "ic"          => "ice crystals", "pe"          => "ice pellets",
  332.                 "gr"          => "hail",         "gs"          => "small hail/snow pellets",
  333.                 "up"          => "unknown precipitation",
  334.  
  335.                 "br"          => "mist",         "fg"          => "fog",
  336.                 "fu"          => "smoke",        "va"          => "volcanic ash",
  337.                 "sa"          => "sand",         "hz"          => "haze",
  338.                 "py"          => "spray",        "du"          => "widespread dust",
  339.  
  340.                 "sq"          => "squall",       "ss"          => "sandstorm",
  341.                 "ds"          => "duststorm",    "po"          => "well developed dust/sand whirls",
  342.                 "fc"          => "funnel cloud",
  343.  
  344.                 "+fc"         => "tornado/waterspout"
  345.             );
  346.             $sensors = array(
  347.                 "rvrno"  => "Runway Visual Range Detector offline",
  348.                 "pwino"  => "Present Weather Identifier offline",
  349.                 "pno"    => "Tipping Bucket Rain Gauge offline",
  350.                 "fzrano" => "Freezing Rain Sensor offline",
  351.                 "tsno"   => "Lightning Detection System offline",
  352.                 "visno"  => "2nd Visibility Sensor offline",
  353.                 "chino"  => "2nd Ceiling Height Indicator offline"
  354.             );
  355.         }
  356.  
  357.         $metarCode = array(
  358.             "report"      => "METAR|SPECI",
  359.             "station"     => "\w{4}",
  360.             "update"      => "(\d{2})?(\d{4})Z",
  361.             "type"        => "AUTO|COR",
  362.             "wind"        => "(\d{3}|VAR|VRB)(\d{2,3})(G(\d{2}))?(\w{2,3})",
  363.             "windVar"     => "(\d{3})V(\d{3})",
  364.             "visibility"  => "(\d{4})|((M|P)?((\d{1,2}|((\d) )?(\d)\/(\d))(SM|KM)))|(CAVOK)",
  365.             "runway"      => "R(\d{2})(\w)?\/(P|M)?(\d{4})(FT)?(V(P|M)?(\d{4})(FT)?)?(\w)?",
  366.             "condition"   => "(-|\+|VC|RE|NSW)?(MI|BC|PR|TS|BL|SH|DR|FZ)?((DZ)|(RA)|(SN)|(SG)|(IC)|(PL)|(GR)|(GS)|(UP))*(BR|FG|FU|VA|DU|SA|HZ|PY)?(PO|SQ|FC|SS|DS)?",
  367.             "clouds"      => "(SKC|CLR|NSC|((FEW|SCT|BKN|OVC|VV)(\d{3})(TCU|CB)?))",
  368.             "temperature" => "(M)?(\d{2})\/((M)?(\d{2})|XX|\/\/)?",
  369.             "pressure"    => "(A)(\d{4})|(Q)(\d{4})",
  370.             "trend"       => "NOSIG|TEMPO|BECMG",
  371.             "remark"      => "RMK"
  372.         );
  373.         
  374.         $remarks = array(
  375.             "nospeci"     => "NOSPECI",
  376.             "autostation" => "AO(1|2)",
  377.             "presschg"    => "PRES(R|F)R",
  378.             "seapressure" => "SLP(\d{3}|NO)",
  379.             "precip"      => "(P|6|7)(\d{4}|\/{4})",
  380.             "snowdepth"   => "4\/(\d{3})",
  381.             "snowequiv"   => "933(\d{3})",
  382.             "cloudtypes"  => "8\/(\d|\/)(\d|\/)(\d|\/)",
  383.             "sunduration" => "98(\d{3})",
  384.             "1htempdew"   => "T(0|1)(\d{3})((0|1)(\d{3}))?",
  385.             "6hmaxtemp"   => "1(0|1)(\d{3})",
  386.             "6hmintemp"   => "2(0|1)(\d{3})",
  387.             "24htemp"     => "4(0|1)(\d{3})(0|1)(\d{3})",
  388.             "3hpresstend" => "5([0-8])(\d{3})",
  389.             "sensors"     => "RVRNO|PWINO|PNO|FZRANO|TSNO|VISNO|CHINO",
  390.             "maintain"    => "[\$]"
  391.         );        
  392.  
  393.         $data = @file($source);
  394.  
  395.         // Check for correct data, 2 lines in size
  396.         if (!$data || !is_array($data) || sizeof($data) < 2) {
  397.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
  398.         } else {
  399.             if (SERVICES_WEATHER_DEBUG) {
  400.                 for ($i = 0; $i < sizeof($data); $i++) {
  401.                     echo $data[$i];
  402.                 }
  403.             }
  404.             // Ok, we have correct data, start with parsing the first line for the last update
  405.             $weatherData = array();
  406.             $weatherData["station"]   = "";
  407.             $weatherData["update"]    = strtotime(trim($data[0])." GMT");
  408.             $weatherData["updateRaw"] = trim($data[0]);
  409.             // and prepare the rest for stepping through
  410.             array_shift($data);
  411.             $metar = explode(" ", preg_replace("/\s{2,}/", " ", implode(" ", $data)));
  412.  
  413.             // Add a few local variables for data processing
  414.             $trendCount = 0;             // If we have trends, we need this
  415.             $pointer    =& $weatherData; // Pointer to the array we add the data to 
  416.             for ($i = 0; $i < sizeof($metar); $i++) {
  417.                 // Check for whitespace and step loop, if nothing's there
  418.                 $metar[$i] = trim($metar[$i]);
  419.                 if (!strlen($metar[$i])) {
  420.                     continue;
  421.                 }
  422.  
  423.                 if (SERVICES_WEATHER_DEBUG) {
  424.                     $tab = str_repeat("\t", 2 - floor((strlen($metar[$i]) + 2) / 8));
  425.                     echo "\"".$metar[$i]."\"".$tab."-> ";
  426.                 }
  427.  
  428.                 $found = false;
  429.                 foreach ($metarCode as $key => $regexp) {
  430.                     // Check if current code matches current metar snippet
  431.                     if (($found = preg_match("/^".$regexp."$/i", $metar[$i], $result)) == true) {
  432.                         switch ($key) {
  433.                             case "station":
  434.                                 $pointer["station"] = $result[0];
  435.                                 unset($metarCode["station"]);
  436.                                 break;
  437.                             case "wind":
  438.                                 // Parse wind data, first the speed, convert from kt to chosen unit
  439.                                 $pointer["wind"] = $this->convertSpeed($result[2], strtolower($result[5]), "mph");
  440.                                 if ($result[1] == "VAR" || $result[1] == "VRB") {
  441.                                     // Variable winds
  442.                                     $pointer["windDegrees"]   = "Variable";
  443.                                     $pointer["windDirection"] = "Variable";
  444.                                 } else {
  445.                                     // Save wind degree and calc direction
  446.                                     $pointer["windDegrees"]   = intval($result[1]);
  447.                                     $pointer["windDirection"] = $compass[round($result[1] / 22.5) % 16];
  448.                                 }
  449.                                 if (is_numeric($result[4])) {
  450.                                     // Wind with gusts...
  451.                                     $pointer["windGust"] = $this->convertSpeed($result[4], strtolower($result[5]), "mph");
  452.                                 }
  453.                                 break;
  454.                             case "windVar":
  455.                                 // Once more wind, now variability around the current wind-direction
  456.                                 $pointer["windVariability"] = array("from" => intval($result[1]), "to" => intval($result[2]));
  457.                                 break;
  458.                             case "visibility":
  459.                                 $pointer["visQualifier"] = "AT";
  460.                                 if (is_numeric($result[1]) && ($result[1] == 9999)) {
  461.                                     // Upper limit of visibility range
  462.                                     $visibility = $this->convertDistance(10, "km", "sm");
  463.                                     $pointer["visQualifier"] = "BEYOND";
  464.                                 } elseif (is_numeric($result[1])) {
  465.                                     // 4-digit visibility in m
  466.                                     $visibility = $this->convertDistance(($result[1]/1000), "km", "sm");
  467.                                 } elseif (!isset($result[11]) || $result[11] != "CAVOK") {
  468.                                     if ($result[3] == "M") {
  469.                                         $pointer["visQualifier"] = "BELOW";
  470.                                     } elseif ($result[3] == "P") {
  471.                                         $pointer["visQualifier"] = "BEYOND";
  472.                                     }
  473.                                     if (is_numeric($result[5])) {
  474.                                         // visibility as one/two-digit number
  475.                                         $visibility = $this->convertDistance($result[5], $result[10], "sm");
  476.                                     } else {
  477.                                         // the y/z part, add if we had a x part (see visibility1)
  478.                                         if (is_numeric($result[7])) {
  479.                                             $visibility = $this->convertDistance($result[7] + $result[8] / $result[9], $result[10], "sm");
  480.                                         } else {
  481.                                             $visibility = $this->convertDistance($result[8] / $result[9], $result[10], "sm");
  482.                                         }
  483.                                     }
  484.                                 } else {
  485.                                     $pointer["visQualifier"] = "BEYOND";
  486.                                     $visibility = $this->convertDistance(10, "km", "sm");
  487.                                     $pointer["clouds"] = array("amount" => "Clear below", "height" => 5000);
  488.                                     $pointer["condition"] = "no significant weather";
  489.                                 }
  490.                                 $pointer["visibility"] = $visibility;
  491.                                 break;
  492.                             case "condition":
  493.                                 // First some basic setups
  494.                                 if (!isset($pointer["condition"])) {
  495.                                     $pointer["condition"] = "";
  496.                                 } elseif (strlen($pointer["condition"]) > 0) {
  497.                                     $pointer["condition"] .= ",";
  498.                                 }
  499.  
  500.                                 if (in_array(strtolower($result[0]), $conditions)) {
  501.                                     // First try matching the complete string
  502.                                     $pointer["condition"] .= " ".$conditions[strtolower($result[0])];
  503.                                 } else {
  504.                                     // No luck, match part by part
  505.                                     array_shift($result);
  506.                                     $result = array_unique($result);
  507.                                     foreach ($result as $condition) {
  508.                                         if (strlen($condition) > 0) {
  509.                                             $pointer["condition"] .= " ".$conditions[strtolower($condition)];
  510.                                         }
  511.                                     }
  512.                                 }
  513.                                 $pointer["condition"] = trim($pointer["condition"]);
  514.                                 break;
  515.                             case "clouds":
  516.                                 if (!isset($pointer["clouds"])) {
  517.                                     $pointer["clouds"] = array();
  518.                                 }
  519.  
  520.                                 if (sizeof($result) == 5) {
  521.                                     // Only amount and height
  522.                                     $cloud = array("amount" => $clouds[strtolower($result[3])], "height" => ($result[4]*100));
  523.                                 }
  524.                                 elseif (sizeof($result) == 6) {
  525.                                     // Amount, height and type
  526.                                     $cloud = array("amount" => $clouds[strtolower($result[3])], "height" => ($result[4]*100), "type" => $clouds[strtolower($result[5])]);
  527.                                 }
  528.                                 else {
  529.                                     // SKC or CLR or NSC
  530.                                     $cloud = array("amount" => $clouds[strtolower($result[0])]);
  531.                                 }
  532.                                 $pointer["clouds"][] = $cloud;
  533.                                 break;
  534.                             case "temperature":
  535.                                 // normal temperature in first part
  536.                                 // negative value
  537.                                 if ($result[1] == "M") {
  538.                                     $result[2] *= -1;
  539.                                 }
  540.                                 $pointer["temperature"] = $this->convertTemperature($result[2], "c", "f");
  541.                                 if (sizeof($result) > 4) {
  542.                                     // same for dewpoint
  543.                                     if ($result[4] == "M") {
  544.                                         $result[5] *= -1;
  545.                                     }
  546.                                     $pointer["dewPoint"] = $this->convertTemperature($result[5], "c", "f");
  547.                                     $pointer["humidity"] = $this->calculateHumidity($result[2], $result[5]);
  548.                                 }
  549.                                 if (isset($pointer["wind"])) {
  550.                                     // Now calculate windchill from temperature and windspeed
  551.                                     $pointer["feltTemperature"] = $this->calculateWindChill($pointer["temperature"], $pointer["wind"]);
  552.                                 }
  553.                                 break;
  554.                             case "pressure":
  555.                                 if ($result[1] == "A") {
  556.                                     // Pressure provided in inches
  557.                                     $pointer["pressure"] = $result[2] / 100;
  558.                                 } elseif ($result[3] == "Q") {
  559.                                     // ... in hectopascal
  560.                                     $pointer["pressure"] = $this->convertPressure($result[4], "hpa", "in");
  561.                                 }
  562.                                 break;
  563.                             case "trend":
  564.                                 // We may have a trend here... extract type and set pointer on
  565.                                 // created new array
  566.                                 if (!isset($weatherData["trend"])) {
  567.                                     $weatherData["trend"] = array();
  568.                                     $weatherData["trend"][$trendCount] = array();
  569.                                 }
  570.                                 $pointer =& $weatherData["trend"][$trendCount];
  571.                                 $trendCount++;
  572.                                 $pointer["type"] = $result[0];
  573.                                 while (isset($metar[$i + 1]) && preg_match("/^(FM|TL|AT)(\d{2})(\d{2})$/i", $metar[$i + 1], $lresult)) {
  574.                                     if ($lresult[1] == "FM") {
  575.                                         $pointer["from"] = $lresult[2].":".$lresult[3];                                
  576.                                     } elseif ($lresult[1] == "TL") {
  577.                                         $pointer["to"] = $lresult[2].":".$lresult[3];
  578.                                     } else {
  579.                                         $pointer["at"] = $lresult[2].":".$lresult[3];
  580.                                     }
  581.                                     // As we have just extracted the time for this trend
  582.                                     // from our METAR, increase field-counter
  583.                                     $i++;
  584.                                 }
  585.                                 break;
  586.                             case "remark":
  587.                                 // Remark part begins
  588.                                 $metarCode = $remarks;
  589.                                 $weatherData["remark"] = array();
  590.                                 break;
  591.                             case "autostation":
  592.                                 // Which autostation do we have here?
  593.                                 if ($result[1] == 0) {
  594.                                     $weatherData["remark"]["autostation"] = "Automatic weatherstation w/o precipitation discriminator";
  595.                                 } else {
  596.                                     $weatherData["remark"]["autostation"] = "Automatic weatherstation w/ precipitation discriminator";
  597.                                 }
  598.                                 unset($metarCode["autostation"]);
  599.                                 break;
  600.                             case "presschg":
  601.                                 // Decoding for rapid pressure changes
  602.                                 if (strtolower($result[1]) == "r") {
  603.                                     $weatherData["remark"]["presschg"] = "Pressure rising rapidly";
  604.                                 } else {
  605.                                     $weatherData["remark"]["presschg"] = "Pressure falling rapidly";
  606.                                 }
  607.                                 unset($metarCode["presschg"]);
  608.                                 break;
  609.                             case "seapressure":
  610.                                 // Pressure at sea level (delivered in hpa)
  611.                                 // Decoding is a bit obscure as 982 gets 998.2
  612.                                 // whereas 113 becomes 1113 -> no real rule here
  613.                                 if (strtolower($result[1]) != "no") {
  614.                                     if ($result[1] > 500) {
  615.                                         $press = 900 + round($result[1] / 100, 1);
  616.                                     } else {
  617.                                         $press = 1000 + $result[1];
  618.                                     }
  619.                                     $weatherData["remark"]["seapressure"] = $this->convertPressure($press, "hpa", "in");
  620.                                 }
  621.                                 unset($metarCode["seapressure"]);
  622.                                 break;
  623.                             case "precip":
  624.                                 // Precipitation in inches
  625.                                 static $hours;
  626.                                 if (!isset($weatherData["precipitation"])) {
  627.                                     $weatherData["precipitation"] = array();
  628.                                     $hours = array("P" => "1", "6" => "3/6", "7" => "24");
  629.                                 }
  630.                                 if (!is_numeric($result[2])) {
  631.                                     $precip = "indeterminable";
  632.                                 } elseif ($result[2] == "0000") {
  633.                                     $precip = "traceable";
  634.                                 }else {
  635.                                     $precip = $result[2] / 100;
  636.                                 }
  637.                                 $weatherData["precipitation"][] = array(
  638.                                     "amount" => $precip,
  639.                                     "hours"  => $hours[$result[1]]
  640.                                 );
  641.                                 break;
  642.                             case "snowdepth":
  643.                                 // Snow depth in inches
  644.                                 $weatherData["remark"]["snowdepth"] = $result[1];
  645.                                 unset($metarCode["snowdepth"]);
  646.                                 break;
  647.                             case "snowequiv":
  648.                                 // Same for equivalent in Water... (inches)
  649.                                 $weatherData["remark"]["snowequiv"] = $result[1] / 10;
  650.                                 unset($metarCode["snowequiv"]);
  651.                                 break;
  652.                             case "cloudtypes":
  653.                                 // Cloud types, haven't found a way for decent decoding (yet)
  654.                                 unset($metarCode["cloudtypes"]);
  655.                                 break;
  656.                             case "sunduration":
  657.                                 // Duration of sunshine (in minutes)
  658.                                 $weatherData["remark"]["sunduration"] = "Total minutes of sunshine: ".$result[1];
  659.                                 unset($metarCode["sunduration"]);
  660.                                 break;
  661.                             case "1htempdew":
  662.                                 // Temperatures in the last hour in C
  663.                                 if ($result[1] == "1") {
  664.                                     $result[2] *= -1;
  665.                                 }
  666.                                 $weatherData["remark"]["1htemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  667.                                 
  668.                                 if (sizeof($result) > 3) {
  669.                                     // same for dewpoint
  670.                                     if ($result[4] == "1") {
  671.                                         $result[5] *= -1;
  672.                                     }
  673.                                     $weatherData["remark"]["1hdew"] = $this->convertTemperature($result[5] / 10, "c", "f");
  674.                                 }
  675.                                 unset($metarCode["1htempdew"]);
  676.                                 break;
  677.                             case "6hmaxtemp":
  678.                                 // Max temperature in the last 6 hours in C
  679.                                 if ($result[1] == "1") {
  680.                                     $result[2] *= -1;
  681.                                 }
  682.                                 $weatherData["remark"]["6hmaxtemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  683.                                 unset($metarCode["6hmaxtemp"]);
  684.                                 break;
  685.                             case "6hmintemp":
  686.                                 // Min temperature in the last 6 hours in C
  687.                                 if ($result[1] == "1") {
  688.                                     $result[2] *= -1;
  689.                                 }
  690.                                 $weatherData["remark"]["6hmintemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  691.                                 unset($metarCode["6hmintemp"]);
  692.                                 break;
  693.                             case "24htemp":
  694.                                 // Max/Min temperatures in the last 24 hours in C
  695.                                 if ($result[1] == "1") {
  696.                                     $result[2] *= -1;
  697.                                 }
  698.                                 $weatherData["remark"]["24hmaxtemp"] = $this->convertTemperature($result[2] / 10, "c", "f");
  699.  
  700.                                 if ($result[3] == "1") {
  701.                                     $result[4] *= -1;
  702.                                 }
  703.                                 $weatherData["remark"]["24hmintemp"] = $this->convertTemperature($result[4] / 10, "c", "f");
  704.                                 unset($metarCode["24htemp"]);
  705.                                 break;
  706.                             case "3hpresstend":
  707.                                 // We don't save the pressure during the day, so no decoding
  708.                                 // possible, sorry
  709.                                 unset($metarCode["3hpresstend"]);
  710.                                 break;
  711.                             case "nospeci":
  712.                                 // No change during the last hour
  713.                                 $weatherData["remark"]["nospeci"] = "No changes in weather conditions";
  714.                                 unset($metarCode["nospeci"]);
  715.                                 break;
  716.                             case "sensors":
  717.                                 // We may have multiple broken sensors, so do not unset
  718.                                 if (!isset($weatherData["remark"]["sensors"])) {
  719.                                     $weatherData["remark"]["sensors"] = array();
  720.                                 }
  721.                                 $weatherData["remark"]["sensors"][strtolower($result[0])] = $sensors[strtolower($result[0])];
  722.                                 break;
  723.                             case "maintain":
  724.                                 $weatherData["remark"]["maintain"] = "Maintainance needed";
  725.                                 unset($metarCode["maintain"]);
  726.                                 break;
  727.                             default:
  728.                                 // Do nothing, just prevent further matching
  729.                                 unset($metarCode[$key]);
  730.                                 break;
  731.                         }
  732.                         if (SERVICES_WEATHER_DEBUG) {
  733.                             echo $key."\n";
  734.                         }
  735.                         break;
  736.                     }
  737.                 }
  738.                 if (!$found) {
  739.                     if (SERVICES_WEATHER_DEBUG) {
  740.                         echo "n/a\n";
  741.                     }
  742.                     if (!isset($weatherData["noparse"])) {
  743.                         $weatherData["noparse"] = array();
  744.                     }
  745.                     $weatherData["noparse"][] = $metar[$i];
  746.                 }
  747.             }
  748.         }
  749.         if (isset($weatherData["noparse"])) {
  750.             $weatherData["noparse"] = implode(" ",  $weatherData["noparse"]);
  751.         }
  752.  
  753.         return $weatherData;
  754.     }
  755.     // }}}
  756.  
  757.     // {{{ _parseForecastData()
  758.     /**
  759.     * Parses the data returned by the provided source and caches it
  760.     *    
  761.     * TAF KLGA 271734Z 271818 11007KT P6SM -RA SCT020 BKN200
  762.     *   FM2300 14007KT P6SM SCT030 BKN150
  763.     *   FM0400 VRB03KT P6SM SCT035 OVC080 PROB30 0509 P6SM -RA BKN035
  764.     *   FM0900 VRB03KT 6SM -RA BR SCT015 OVC035
  765.     *       TEMPO 1215 5SM -RA BR SCT009 BKN015
  766.     *       BECMG 1517 16007KT P6SM NSW SCT015 BKN070
  767.     *
  768.     * @param    string                      $source
  769.     * @return   PEAR_Error|array
  770.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  771.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  772.     * @access   private
  773.     */
  774.     function _parseForecastData($source)
  775.     {
  776.         static $compass;
  777.         static $clouds;
  778.         static $conditions;
  779.         static $sensors;
  780.         if (!isset($compass)) {
  781.             $compass = array(
  782.                 "N", "NNE", "NE", "ENE",
  783.                 "E", "ESE", "SE", "SSE",
  784.                 "S", "SSW", "SW", "WSW",
  785.                 "W", "WNW", "NW", "NNW"
  786.             );
  787.             $clouds    = array(
  788.                 "skc"         => "sky clear",
  789.                 "nsc"         => "no significant cloud",
  790.                 "few"         => "few",
  791.                 "sct"         => "scattered",
  792.                 "bkn"         => "broken",
  793.                 "ovc"         => "overcast",
  794.                 "vv"          => "vertical visibility",
  795.                 "tcu"         => "Towering Cumulus",
  796.                 "cb"          => "Cumulonimbus",
  797.                 "clr"         => "clear below 12,000 ft"
  798.             );
  799.             $conditions = array(
  800.                 "+"           => "heavy",        "-"           => "light",
  801.  
  802.                 "vc"          => "vicinity",     "re"          => "recent",
  803.                 "nsw"         => "no significant weather",
  804.  
  805.                 "mi"          => "shallow",      "bc"          => "patches",
  806.                 "pr"          => "partial",      "ts"          => "thunderstorm",
  807.                 "bl"          => "blowing",      "sh"          => "showers",
  808.                 "dr"          => "low drifting", "fz"          => "freezing",
  809.  
  810.                 "dz"          => "drizzle",      "ra"          => "rain",
  811.                 "sn"          => "snow",         "sg"          => "snow grains",
  812.                 "ic"          => "ice crystals", "pe"          => "ice pellets",
  813.                 "gr"          => "hail",         "gs"          => "small hail/snow pellets",
  814.                 "up"          => "unknown precipitation",
  815.  
  816.                 "br"          => "mist",         "fg"          => "fog",
  817.                 "fu"          => "smoke",        "va"          => "volcanic ash",
  818.                 "sa"          => "sand",         "hz"          => "haze",
  819.                 "py"          => "spray",        "du"          => "widespread dust",
  820.  
  821.                 "sq"          => "squall",       "ss"          => "sandstorm",
  822.                 "ds"          => "duststorm",    "po"          => "well developed dust/sand whirls",
  823.                 "fc"          => "funnel cloud",
  824.  
  825.                 "+fc"         => "tornado/waterspout"
  826.             );
  827.         }
  828.  
  829.         $tafCode = array(
  830.             "report"      => "TAF|AMD",
  831.             "station"     => "\w{4}",
  832.             "update"      => "(\d{2})?(\d{4})Z",
  833.             "valid"       => "(\d{2})(\d{2})(\d{2})",
  834.             "wind"        => "(\d{3}|VAR|VRB)(\d{2,3})(G(\d{2}))?(\w{2,3})",
  835.             "visibility"  => "(\d{4})|((M|P)?((\d{1,2}|((\d) )?(\d)\/(\d))(SM|KM)))|(CAVOK)",
  836.             "condition"   => "(-|\+|VC|RE|NSW)?(MI|BC|PR|TS|BL|SH|DR|FZ)?((DZ)|(RA)|(SN)|(SG)|(IC)|(PL)|(GR)|(GS)|(UP))*(BR|FG|FU|VA|DU|SA|HZ|PY)?(PO|SQ|FC|SS|DS)?",
  837.             "clouds"      => "(SKC|CLR|NSC|((FEW|SCT|BKN|OVC|VV)(\d{3})(TCU|CB)?))",
  838.             "windshear"   => "WS(\d{3})\/(\d{3})(\d{2,3})(\w{2,3})",
  839.             "tempmax"     => "TX(\d{2})\/(\d{2})(\w)",
  840.             "tempmin"     => "TN(\d{2})\/(\d{2})(\w)",
  841.             "tempmaxmin"  => "TX(\d{2})\/(\d{2})(\w)TN(\d{2})\/(\d{2})(\w)",
  842.             "from"        => "FM(\d{2})(\d{2})",
  843.             "fmc"         => "(PROB|BECMG|TEMPO)(\d{2})?"
  844.         );
  845.  
  846.         $data = @file($source);
  847.  
  848.         // Check for correct data
  849.         if (!$data || !is_array($data) || sizeof($data) < 2) {
  850.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA, __FILE__, __LINE__);
  851.         } else {
  852.             if (SERVICES_WEATHER_DEBUG) {
  853.                 for ($i = 0; $i < sizeof($data); $i++) {
  854.                     echo $data[$i];
  855.                 }
  856.             }
  857.             // Ok, we have correct data, start with parsing the first line for the last update
  858.             $forecastData = array();
  859.             $forecastData["station"]   = "";
  860.             $forecastData["update"]    = strtotime(trim($data[0])." GMT");
  861.             $forecastData["updateRaw"] = trim($data[0]);
  862.             // and prepare the rest for stepping through
  863.             array_shift($data);
  864.             $taf = explode(" ", preg_replace("/\s{2,}/", " ", implode(" ", $data)));
  865.  
  866.             // Add a few local variables for data processing
  867.             $fromTime =  "";            // The timeperiod the data gets added to
  868.             $fmcCount =  0;             // If we have FMCs (Forecast Meteorological Conditions), we need this
  869.             $pointer  =& $forecastData; // Pointer to the array we add the data to 
  870.             for ($i = 0; $i < sizeof($taf); $i++) {
  871.                 // Check for whitespace and step loop, if nothing's there
  872.                 $taf[$i] = trim($taf[$i]);
  873.                 if (!strlen($taf[$i])) {
  874.                     continue;
  875.                 }
  876.  
  877.                 if (SERVICES_WEATHER_DEBUG) {
  878.                     $tab = str_repeat("\t", 2 - floor((strlen($taf[$i]) + 2) / 8));
  879.                     echo "\"".$taf[$i]."\"".$tab."-> ";
  880.                 }
  881.  
  882.                 $found = false;
  883.                 foreach ($tafCode as $key => $regexp) {
  884.                     // Check if current code matches current taf snippet
  885.                     if (($found = preg_match("/^".$regexp."$/i", $taf[$i], $result)) == true) {
  886.                         $insert = array();
  887.                         switch ($key) {
  888.                             case "station":
  889.                                 $pointer["station"] = $result[0];
  890.                                 unset($tafCode["station"]);
  891.                                 break;
  892.                             case "valid":
  893.                                 $pointer["validRaw"] = $result[0];
  894.                                 // Generates the timeperiod the report is valid for
  895.                                 list($year, $month, $day) = explode("-", date("Y-m-d", $forecastData["update"]));
  896.                                 // Date is in next month
  897.                                 if ($result[1] < $day) {
  898.                                     $month++;
  899.                                 }
  900.                                 $pointer["validFrom"] = gmmktime($result[2], 0, 0, $month, $result[1], $year);
  901.                                 // Valid time ends next day
  902.                                 if ($result[2] >= $result[3]) {
  903.                                     $result[1]++;
  904.                                 }
  905.                                 $pointer["validTo"]   = gmmktime($result[3], 0, 0, $month, $result[1], $year);
  906.                                 unset($tafCode["valid"]);
  907.                                 // Now the groups will start, so initialize the time groups
  908.                                 $pointer["time"] = array();
  909.                                 $fromTime = $result[2].":00";
  910.                                 $pointer["time"][$fromTime] = array();
  911.                                 // Set pointer to the first timeperiod
  912.                                 $pointer =& $pointer["time"][$fromTime];
  913.                                 break;
  914.                             case "wind":
  915.                                 // Parse wind data, first the speed, convert from kt to chosen unit
  916.                                 $pointer["wind"] = $this->convertSpeed($result[2], strtolower($result[5]), "mph");
  917.                                 if ($result[1] == "VAR" || $result[1] == "VRB") {
  918.                                     // Variable winds
  919.                                     $pointer["windDegrees"]   = "Variable";
  920.                                     $pointer["windDirection"] = "Variable";
  921.                                 } else {
  922.                                     // Save wind degree and calc direction
  923.                                     $pointer["windDegrees"]   = $result[1];
  924.                                     $pointer["windDirection"] = $compass[round($result[1] / 22.5) % 16];
  925.                                 }
  926.                                 if (is_numeric($result[4])) {
  927.                                     // Wind with gusts...
  928.                                     $pointer["windGust"] = $this->convertSpeed($result[4], strtolower($result[5]), "mph");
  929.                                 }
  930.                                 if (isset($probability)) {
  931.                                     $pointer["windProb"] = $probability;
  932.                                     unset($probability);
  933.                                 }
  934.                                 break;
  935.                             case "visibility":
  936.                                 $pointer["visQualifier"] = "AT";
  937.                                 if (is_numeric($result[1]) && ($result[1] == 9999)) {
  938.                                     // Upper limit of visibility range
  939.                                     $visibility = $this->convertDistance(10, "km", "sm");
  940.                                     $pointer["visQualifier"] = "BEYOND";
  941.                                 } elseif (is_numeric($result[1])) {
  942.                                     // 4-digit visibility in m
  943.                                     $visibility = $this->convertDistance(($result[1]/1000), "km", "sm");
  944.                                 } elseif (!isset($result[11]) || $result[11] != "CAVOK") {
  945.                                     if ($result[3] == "M") {
  946.                                         $pointer["visQualifier"] = "BELOW";
  947.                                     } elseif ($result[3] == "P") {
  948.                                         $pointer["visQualifier"] = "BEYOND";
  949.                                     }
  950.                                     if (is_numeric($result[5])) {
  951.                                         // visibility as one/two-digit number
  952.                                         $visibility = $this->convertDistance($result[5], $result[10], "sm");
  953.                                     } else {
  954.                                         // the y/z part, add if we had a x part (see visibility1)
  955.                                         if (is_numeric($result[7])) {
  956.                                             $visibility = $this->convertDistance($result[7] + $result[8] / $result[9], $result[10], "sm");
  957.                                         } else {
  958.                                             $visibility = $this->convertDistance($result[8] / $result[9], $result[10], "sm");
  959.                                         }
  960.                                     }
  961.                                 } else {
  962.                                     $pointer["visQualifier"] = "BEYOND";
  963.                                     $visibility = $this->convertDistance(10, "km", "sm");
  964.                                     $pointer["clouds"] = array("amount" => "none", "height" => "below 5000ft");
  965.                                     $pointer["condition"] = "no significant weather";
  966.                                 }
  967.                                 if (isset($probability)) {
  968.                                     $pointer["visProb"] = $probability;
  969.                                     unset($probability);
  970.                                 }
  971.                                 $pointer["visibility"] = $visibility;
  972.                                 break;
  973.                             case "condition":
  974.                                 // First some basic setups
  975.                                 if (!isset($pointer["condition"])) {
  976.                                     $pointer["condition"] = "";
  977.                                 } elseif (strlen($pointer["condition"]) > 0) {
  978.                                     $pointer["condition"] .= ",";
  979.                                 }
  980.  
  981.                                 if (in_array(strtolower($result[0]), $conditions)) {
  982.                                     // First try matching the complete string
  983.                                     $pointer["condition"] .= " ".$conditions[strtolower($result[0])];
  984.                                 } else {
  985.                                     // No luck, match part by part
  986.                                     array_shift($result);
  987.                                     $result = array_unique($result);
  988.                                     foreach ($result as $condition) {
  989.                                         if (strlen($condition) > 0) {
  990.                                             $pointer["condition"] .= " ".$conditions[strtolower($condition)];
  991.                                         }
  992.                                     }
  993.                                 }
  994.                                 $pointer["condition"] = trim($pointer["condition"]);
  995.                                 if (isset($probability)) {
  996.                                     $pointer["condition"] .= " (".$probability."% prob.)";
  997.                                     unset($probability);
  998.                                 }
  999.                                 break;
  1000.                             case "clouds":
  1001.                                 if (!isset($pointer["clouds"])) {
  1002.                                     $pointer["clouds"] = array();
  1003.                                 }
  1004.  
  1005.                                 if (sizeof($result) == 5) {
  1006.                                     // Only amount and height
  1007.                                     $cloud = array("amount" => $clouds[strtolower($result[3])], "height" => ($result[4] * 100));
  1008.                                 }
  1009.                                 elseif (sizeof($result) == 6) {
  1010.                                     // Amount, height and type
  1011.                                     $cloud = array("amount" => $clouds[strtolower($result[3])], "height" => ($result[4] * 100), "type" => $clouds[strtolower($result[5])]);
  1012.                                 }
  1013.                                 else {
  1014.                                     // SKC or CLR or NSC
  1015.                                     $cloud = array("amount" => $clouds[strtolower($result[0])]);
  1016.                                 }
  1017.                                 if(isset($probability)) {
  1018.                                     $cloud["prob"] = $probability;
  1019.                                     unset($probability);
  1020.                                 }
  1021.                                 $pointer["clouds"][] = $cloud;
  1022.                                 break;
  1023.                             case "windshear":
  1024.                                 // Parse windshear, if available
  1025.                                 $pointer["windshear"]          = $this->convertSpeed($result[3], strtolower($result[4]), "mph");
  1026.                                 $pointer["windshearHeight"]    = $result[1] * 100;
  1027.                                 $pointer["windshearDegrees"]   = $result[2];
  1028.                                 $pointer["windshearDirection"] = $compass[round($result[2] / 22.5) % 16];
  1029.                                 break;
  1030.                             case "tempmax":
  1031.                                 $forecastData["temperatureHigh"] = $this->convertTemperature($result[1], "c", "f");
  1032.                                 break;
  1033.                             case "tempmin":
  1034.                                 // Parse max/min temperature
  1035.                                 $forecastData["temperatureLow"]  = $this->convertTemperature($result[1], "c", "f");
  1036.                                 break;
  1037.                             case "tempmaxmin":
  1038.                                 $forecastData["temperatureHigh"] = $this->convertTemperature($result[1], "c", "f");
  1039.                                 $forecastData["temperatureLow"]  = $this->convertTemperature($result[4], "c", "f");
  1040.                                 break;
  1041.                             case "from":
  1042.                                 // Next timeperiod is coming up, prepare array and
  1043.                                 // set pointer accordingly
  1044.                                 $fromTime = $result[1].":".$result[2];
  1045.                                 $forecastData["time"][$fromTime] = array();
  1046.                                 $fmcCount = 0;
  1047.                                 $pointer =& $forecastData["time"][$fromTime];
  1048.                                 break;
  1049.                             case "fmc";
  1050.                                 // Test, if this is a probability for the next FMC                                
  1051.                                 if (preg_match("/^BECMG|TEMPO$/i", $taf[$i + 1], $lresult)) {
  1052.                                     // Set type to BECMG or TEMPO
  1053.                                     $type = $lresult[0];
  1054.                                     // Set probability
  1055.                                     $probability = $result[2];
  1056.                                     // Now extract time for this group
  1057.                                     preg_match("/^(\d{2})(\d{2})$/i", $taf[$i + 2], $lresult);
  1058.                                     $from = $lresult[1].":00";
  1059.                                     $to   = $lresult[2].":00";
  1060.                                     $to   = ($to == "24:00") ? "00:00" : $to;
  1061.                                     // As we now have type, probability and time for this FMC
  1062.                                     // from our TAF, increase field-counter
  1063.                                     $i += 2;
  1064.                                 } elseif (preg_match("/^(\d{2})(\d{2})$/i", $taf[$i + 1], $lresult)) {
  1065.                                     // Normal group, set type and use extracted time
  1066.                                     $type = $result[1];
  1067.                                     // Check for PROBdd
  1068.                                     if (isset($result[2])) {
  1069.                                         $probability = $result[2];
  1070.                                     }
  1071.                                     $from = $lresult[1].":00";
  1072.                                     $to   = $lresult[2].":00";
  1073.                                     $to   = ($to == "24:00") ? "00:00" : $to;
  1074.                                     // Same as above, we have a time for this FMC from our TAF, 
  1075.                                     // increase field-counter
  1076.                                     $i += 1;
  1077.                                 } else {
  1078.                                     // This is either a PROBdd or a malformed TAF
  1079.                                     if (isset($result[2])) {
  1080.                                         $probability = $result[2];
  1081.                                     }
  1082.                                 }
  1083.  
  1084.                                 // Handle the FMC, generate neccessary array if it's the first...
  1085.                                 if (isset($type)) {
  1086.                                     if (!isset($forecastData["time"][$fromTime]["fmc"])) {
  1087.                                         $forecastData["time"][$fromTime]["fmc"] = array();
  1088.                                     }
  1089.                                     $forecastData["time"][$fromTime]["fmc"][$fmcCount] = array();
  1090.                                     // ...and set pointer.
  1091.                                     $pointer =& $forecastData["time"][$fromTime]["fmc"][$fmcCount];
  1092.                                     $fmcCount++;
  1093.                                     // Insert data
  1094.                                     $pointer["type"] = $type;
  1095.                                     $pointer["from"] = $from;
  1096.                                     $pointer["to"]   = $to;
  1097.                                     unset($type, $from, $to);
  1098.                                     if (isset($probability)) {
  1099.                                         $pointer["probability"] = $probability;
  1100.                                         unset($probability);
  1101.                                     }
  1102.                                 }
  1103.                                 break;
  1104.                             default:
  1105.                                 // Do nothing
  1106.                                 break;
  1107.                         }
  1108.                         if (SERVICES_WEATHER_DEBUG) {
  1109.                             echo $key."\n";
  1110.                         }
  1111.                         break;
  1112.                     }
  1113.                 }
  1114.                 if (!$found) {
  1115.                     if (SERVICES_WEATHER_DEBUG) {
  1116.                         echo "n/a\n";
  1117.                     }
  1118.                     if (!isset($forecastData["noparse"])) {
  1119.                         $forecastData["noparse"] = array();
  1120.                     }
  1121.                     $forecastData["noparse"][] = $taf[$i];
  1122.                 }
  1123.             }
  1124.         }
  1125.         if (isset($forecastData["noparse"])) {
  1126.             $forecastData["noparse"] = implode(" ",  $forecastData["noparse"]);
  1127.         }
  1128.  
  1129.         return $forecastData;
  1130.     }
  1131.     // }}}
  1132.  
  1133.     // {{{ _convertReturn()
  1134.     /**
  1135.     * Converts the data in the return array to the desired units and/or
  1136.     * output format. 
  1137.     *
  1138.     * @param    array                       $target
  1139.     * @param    string                      $units
  1140.     * @param    string                      $location
  1141.     * @access   private
  1142.     */
  1143.     function _convertReturn(&$target, $units, $location)
  1144.     {
  1145.         if (is_array($target)) {
  1146.             foreach ($target as $key => $val) {
  1147.                 if (is_array($val)) {
  1148.                     // Another array detected, so recurse into it to convert the units
  1149.                     $this->_convertReturn($target[$key], $units, $location);
  1150.                 } else {
  1151.                     switch ($key) {
  1152.                         case "station":
  1153.                             $newVal = $location["name"];
  1154.                             break;
  1155.                         case "update":
  1156.                         case "validFrom":
  1157.                         case "validTo":
  1158.                             $newVal = gmdate(trim($this->_dateFormat." ".$this->_timeFormat), $val);
  1159.                             break;
  1160.                         case "wind":
  1161.                         case "windGust":
  1162.                         case "windshear":
  1163.                             $newVal = $this->convertSpeed($val, "mph", $units["wind"]);
  1164.                             break;
  1165.                         case "visibility":
  1166.                             $newVal = $this->convertDistance($val, "sm", $units["vis"]);
  1167.                             break;
  1168.                         case "height":
  1169.                         case "windshearHeight":
  1170.                             $newVal = $this->convertDistance($val, "ft", $units["height"]);
  1171.                             break;
  1172.                         case "temperature":
  1173.                         case "temperatureHigh":
  1174.                         case "temperatureLow":
  1175.                         case "dewPoint":
  1176.                         case "feltTemperature":
  1177.                             $newVal = $this->convertTemperature($val, "f", $units["temp"]);
  1178.                             break;
  1179.                         case "pressure":
  1180.                             $newVal = $this->convertPressure($val, "in", $units["pres"]);
  1181.                             break;
  1182.                         case "amount":
  1183.                         case "snowdepth":
  1184.                         case "snowequiv":
  1185.                             if (is_numeric($val)) {
  1186.                                 $newVal = $this->convertPressure($val, "in", $units["rain"]);
  1187.                             } else {
  1188.                                 $newVal = $val;
  1189.                             }
  1190.                             break;
  1191.                         case "seapressure":
  1192.                             $newVal = $this->convertPressure($val, "in", $units["pres"]);
  1193.                             break;
  1194.                         case "1htemp":
  1195.                         case "1hdew":
  1196.                         case "6hmaxtemp":
  1197.                         case "6hmintemp":
  1198.                         case "24hmaxtemp":
  1199.                         case "24hmintemp":
  1200.                             $newVal = $this->convertTemperature($val, "f", $units["temp"]);
  1201.                             break;
  1202.                         default:
  1203.                             continue 2;
  1204.                             break;
  1205.                     }
  1206.                     $target[$key] = $newVal;
  1207.                 }
  1208.             }
  1209.         }
  1210.     }
  1211.     // }}}
  1212.  
  1213.     // {{{ searchLocation()
  1214.     /**
  1215.     * Searches IDs for given location, returns array of possible locations
  1216.     * or single ID
  1217.     *
  1218.     * @param    string|array                $location
  1219.     * @param    bool                        $useFirst       If set, first ID of result-array is returned
  1220.     * @return   PEAR_Error|array|string
  1221.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  1222.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  1223.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  1224.     * @access   public
  1225.     */
  1226.     function searchLocation($location, $useFirst = false)
  1227.     {
  1228.         if (!isset($this->_db) || !DB::isConnection($this->_db)) {
  1229.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED, __FILE__, __LINE__);
  1230.         }
  1231.         
  1232.         if (is_string($location)) {
  1233.             // Try to part search string in name, state and country part
  1234.             // and build where clause from it for the select
  1235.             $location = explode(",", $location);
  1236.             if (sizeof($location) >= 1) {
  1237.                 $where  = "LOWER(name) LIKE '%".strtolower(trim($location[0]))."%'";
  1238.             }
  1239.             if (sizeof($location) == 2) {
  1240.                 $where .= " AND LOWER(country) LIKE '%".strtolower(trim($location[1]))."%'";
  1241.             } elseif (sizeof($location) == 3) {
  1242.                 $where .= " AND LOWER(state) LIKE '%".strtolower(trim($location[1]))."%'";
  1243.                 $where .= " AND LOWER(country) LIKE '%".strtolower(trim($location[2]))."%'";
  1244.             }
  1245.                 
  1246.             // Create select, locations with ICAO first
  1247.             $select = "SELECT icao, name, state, country, latitude, longitude ".
  1248.                       "FROM metarLocations ".
  1249.                       "WHERE ".$where." ".
  1250.                       "ORDER BY icao DESC";
  1251.             $result = $this->_db->query($select);
  1252.             // Check result for validity
  1253.             if (DB::isError($result)) {
  1254.                 return $result;
  1255.             } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) {
  1256.                 return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1257.             }
  1258.             
  1259.             // Result is valid, start preparing the return
  1260.             $icao = array();
  1261.             while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) {
  1262.                 $locicao = $row["icao"];
  1263.                 // First the name of the location
  1264.                 if (!strlen($row["state"])) {
  1265.                     $locname = $row["name"].", ".$row["country"];
  1266.                 } else {
  1267.                     $locname = $row["name"].", ".$row["state"].", ".$row["country"];
  1268.                 }
  1269.                 if ($locicao != "----") {
  1270.                     // We have a location with ICAO
  1271.                     $icao[$locicao] = $locname;
  1272.                 } else {
  1273.                     // No ICAO, try finding the nearest airport
  1274.                     $locicao = $this->searchAirport($row["latitude"], $row["longitude"]);
  1275.                     if (!isset($icao[$locicao])) {
  1276.                         $icao[$locicao] = $locname;
  1277.                     }
  1278.                 }
  1279.             }
  1280.             // Only one result? Return as string
  1281.             if (sizeof($icao) == 1) {
  1282.                 $icao = key($icao);
  1283.             }
  1284.         } elseif (is_array($location)) {
  1285.             // Location was provided as coordinates, search nearest airport
  1286.             $icao = $this->searchAirport($location[0], $location[1]);
  1287.         } else {
  1288.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__);
  1289.         }
  1290.  
  1291.         return $icao;
  1292.     }
  1293.     // }}}
  1294.  
  1295.     // {{{ searchLocationByCountry()
  1296.     /**
  1297.     * Returns IDs with location-name for a given country or all available
  1298.     * countries, if no value was given 
  1299.     *
  1300.     * @param    string                      $country
  1301.     * @return   PEAR_Error|array
  1302.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  1303.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  1304.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_WRONG_SERVER_DATA
  1305.     * @access   public
  1306.     */
  1307.     function searchLocationByCountry($country = "")
  1308.     {
  1309.         if (!isset($this->_db) || !DB::isConnection($this->_db)) {
  1310.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED, __FILE__, __LINE__);
  1311.         }
  1312.  
  1313.         // Return the available countries as no country was given
  1314.         if (!strlen($country)) {
  1315.             $select = "SELECT DISTINCT(country) ".
  1316.                       "FROM metarAirports ".
  1317.                       "ORDER BY country ASC";
  1318.             $countries = $this->_db->getCol($select);
  1319.  
  1320.             // As $countries is either an error or the true result,
  1321.             // we can just return it
  1322.             return $countries;
  1323.         }
  1324.  
  1325.         // Now for the real search
  1326.         $select = "SELECT icao, name, state, country ".
  1327.                   "FROM metarAirports ".
  1328.                   "WHERE LOWER(country) LIKE '%".strtolower(trim($country))."%' ".
  1329.                   "ORDER BY name ASC";
  1330.         $result = $this->_db->query($select);
  1331.         // Check result for validity
  1332.         if (DB::isError($result)) {
  1333.             return $result;
  1334.         } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) {
  1335.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1336.         }
  1337.  
  1338.         // Construct the result
  1339.         $locations = array();
  1340.         while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) {
  1341.             $locicao = $row["icao"];
  1342.             // First the name of the location
  1343.             if (!strlen($row["state"])) {
  1344.                 $locname = $row["name"].", ".$row["country"];
  1345.             } else {
  1346.                 $locname = $row["name"].", ".$row["state"].", ".$row["country"];
  1347.             }
  1348.             $locations[$locicao] = $locname;
  1349.         }
  1350.  
  1351.         return $locations;
  1352.     }
  1353.     // }}}
  1354.  
  1355.     // {{{ searchAirport()
  1356.     /**
  1357.     * Searches the nearest airport(s) for given coordinates, returns array
  1358.     * of IDs or single ID
  1359.     *
  1360.     * @param    float                       $latitude
  1361.     * @param    float                       $longitude
  1362.     * @param    int                         $numResults
  1363.     * @return   PEAR_Error|array|string
  1364.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION
  1365.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED
  1366.     * @throws   PEAR_Error::SERVICES_WEATHER_ERROR_INVALID_LOCATION
  1367.     * @access   public
  1368.     */
  1369.     function searchAirport($latitude, $longitude, $numResults = 1)
  1370.     {
  1371.         if (!isset($this->_db) || !DB::isConnection($this->_db)) {
  1372.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_DB_NOT_CONNECTED, __FILE__, __LINE__);
  1373.         }
  1374.         if (!is_numeric($latitude) || !is_numeric($longitude)) {
  1375.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_INVALID_LOCATION, __FILE__, __LINE__);
  1376.         }           
  1377.         
  1378.         // Get all airports
  1379.         $select = "SELECT icao, x, y, z FROM metarAirports";
  1380.         $result = $this->_db->query($select);
  1381.         if (DB::isError($result)) {
  1382.             return $result;
  1383.         } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) {
  1384.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1385.         }
  1386.  
  1387.         // Result is valid, start search
  1388.         // Initialize values
  1389.         $min_dist = null;
  1390.         $query    = $this->polar2cartesian($latitude, $longitude);
  1391.         $search   = array("dist" => array(), "icao" => array());
  1392.         while (($row = $result->fetchRow(DB_FETCHMODE_ASSOC)) != null) {
  1393.             $icao = $row["icao"];
  1394.             $air  = array($row["x"], $row["y"], $row["z"]);
  1395.  
  1396.             $dist = 0;
  1397.             $d = 0;
  1398.             // Calculate distance of query and current airport
  1399.             // break off, if distance is larger than current $min_dist
  1400.             for($d; $d < sizeof($air); $d++) {
  1401.                 $t = $air[$d] - $query[$d];
  1402.                 $dist += pow($t, 2);
  1403.                 if ($min_dist != null && $dist > $min_dist) {
  1404.                     break;
  1405.                 }
  1406.             }
  1407.  
  1408.             if ($d >= sizeof($air)) {
  1409.                 // Ok, current airport is one of the nearer locations
  1410.                 // add to result-array
  1411.                 $search["dist"][] = $dist;
  1412.                 $search["icao"][] = $icao;
  1413.                 // Sort array for distance
  1414.                 array_multisort($search["dist"], SORT_NUMERIC, SORT_ASC, $search["icao"], SORT_STRING, SORT_ASC);
  1415.                 // If array is larger then desired results, chop off last one
  1416.                 if (sizeof($search["dist"]) > $numResults) {
  1417.                     array_pop($search["dist"]);
  1418.                     array_pop($search["icao"]);
  1419.                 }
  1420.                 $min_dist = max($search["dist"]);
  1421.             }
  1422.         }
  1423.         if ($numResults == 1) {
  1424.             // Only one result wanted, return as string
  1425.             return $search["icao"][0];
  1426.         } elseif ($numResults > 1) {
  1427.             // Return found locations
  1428.             return $search["icao"];
  1429.         } else {
  1430.             return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1431.         }
  1432.     }
  1433.     // }}}
  1434.  
  1435.     // {{{ getLocation()
  1436.     /**
  1437.     * Returns the data for the location belonging to the ID
  1438.     *
  1439.     * @param    string                      $id
  1440.     * @return   PEAR_Error|array
  1441.     * @throws   PEAR_Error
  1442.     * @access   public
  1443.     */
  1444.     function getLocation($id = "")
  1445.     {
  1446.         $status = $this->_checkLocationID($id);
  1447.  
  1448.         if (Services_Weather::isError($status)) {
  1449.             return $status;
  1450.         }
  1451.  
  1452.         $locationReturn = array();
  1453.  
  1454.         if ($this->_cacheEnabled && ($location = $this->_cache->get("METAR-".$id, "location"))) {
  1455.             // Grab stuff from cache
  1456.             $this->_location = $location;
  1457.             $locationReturn["cache"] = "HIT";
  1458.         } elseif (isset($this->_db) && DB::isConnection($this->_db)) {
  1459.             // Get data from DB
  1460.             $select = "SELECT icao, name, state, country, latitude, longitude, elevation ".
  1461.                       "FROM metarAirports WHERE icao='".$id."'";
  1462.             $result = $this->_db->query($select);
  1463.  
  1464.             if (DB::isError($result)) {
  1465.                 return $result;
  1466.             } elseif (strtolower(get_class($result)) != "db_result" || $result->numRows() == 0) {
  1467.                 return Services_Weather::raiseError(SERVICES_WEATHER_ERROR_UNKNOWN_LOCATION, __FILE__, __LINE__);
  1468.             }
  1469.             // Result is ok, put things into object
  1470.             $this->_location = $result->fetchRow(DB_FETCHMODE_ASSOC);
  1471.  
  1472.             if ($this->_cacheEnabled) {
  1473.                 // ...and cache it
  1474.                 $expire = constant("SERVICES_WEATHER_EXPIRES_LOCATION");
  1475.                 $this->_cache->extSave("METAR-".$id, $this->_location, "", $expire, "location");
  1476.             }
  1477.  
  1478.             $locationReturn["cache"] = "MISS";
  1479.         } else {
  1480.             $this->_location = array(
  1481.                 "name"      => $id,
  1482.                 "state"     => "",
  1483.                 "country"   => "",
  1484.                 "latitude"  => "",
  1485.                 "longitude" => "",
  1486.                 "elevation" => ""
  1487.             );
  1488.         }
  1489.         // Stuff name-string together
  1490.         if (strlen($this->_location["state"]) && strlen($this->_location["country"])) {
  1491.             $locname = $this->_location["name"].", ".$this->_location["state"].", ".$this->_location["country"];
  1492.         } elseif (strlen($this->_location["country"])) {
  1493.             $locname = $this->_location["name"].", ".$this->_location["country"];
  1494.         } else {
  1495.             $locname = $this->_location["name"];
  1496.         }
  1497.         $locationReturn["name"]      = $locname;
  1498.         $locationReturn["latitude"]  = $this->_location["latitude"];
  1499.         $locationReturn["longitude"] = $this->_location["longitude"];
  1500.         $locationReturn["elevation"] = $this->_location["elevation"];
  1501.  
  1502.         return $locationReturn;
  1503.     }
  1504.     // }}}
  1505.  
  1506.     // {{{ getWeather()
  1507.     /**
  1508.     * Returns the weather-data for the supplied location
  1509.     *
  1510.     * @param    string                      $id
  1511.     * @param    string                      $unitsFormat
  1512.     * @return   PHP_Error|array
  1513.     * @throws   PHP_Error
  1514.     * @access   public
  1515.     */
  1516.     function getWeather($id = "", $unitsFormat = "")
  1517.     {
  1518.         $id     = strtoupper($id);
  1519.         $status = $this->_checkLocationID($id);
  1520.  
  1521.         if (Services_Weather::isError($status)) {
  1522.             return $status;
  1523.         }
  1524.  
  1525.         // Get other data
  1526.         $units    = $this->getUnitsFormat($unitsFormat);
  1527.         $location = $this->getLocation($id);
  1528.  
  1529.         if ($this->_cacheEnabled && ($weather = $this->_cache->get("METAR-".$id, "weather"))) {
  1530.             // Wee... it was cached, let's have it...
  1531.             $weatherReturn  = $weather;
  1532.             $this->_weather = $weatherReturn;
  1533.             $weatherReturn["cache"] = "HIT";
  1534.         } else {
  1535.             // Set the source
  1536.             if ($this->_sourceMetar == "file") {
  1537.                 $source = realpath($this->_sourcePathMetar."/".$id.".TXT");
  1538.             } else {
  1539.                 $source = $this->_sourcePathMetar."/".$id.".TXT";
  1540.             }
  1541.  
  1542.             // Download and parse weather
  1543.             $weatherReturn  = $this->_parseWeatherData($source);
  1544.  
  1545.             if (Services_Weather::isError($weatherReturn)) {
  1546.                 return $weatherReturn;
  1547.             }
  1548.             if ($this->_cacheEnabled) {
  1549.                 // Cache weather
  1550.                 $expire = constant("SERVICES_WEATHER_EXPIRES_WEATHER");
  1551.                 $this->_cache->extSave("METAR-".$id, $weatherReturn, $unitsFormat, $expire, "weather");
  1552.             }
  1553.             $this->_weather = $weatherReturn;
  1554.             $weatherReturn["cache"] = "MISS";
  1555.         }
  1556.  
  1557.         $this->_convertReturn($weatherReturn, $units, $location);
  1558.  
  1559.         return $weatherReturn;
  1560.     }
  1561.     // }}}
  1562.     
  1563.     // {{{ getForecast()
  1564.     /**
  1565.     * METAR provides no forecast per se, we use the TAF reports to generate
  1566.     * a forecast for the announced timeperiod
  1567.     *
  1568.     * @param    string                      $id
  1569.     * @param    int                         $days           Ignored, not applicable
  1570.     * @param    string                      $unitsFormat
  1571.     * @return   PEAR_Error|array
  1572.     * @throws   PEAR_Error
  1573.     * @access   public
  1574.     */
  1575.     function getForecast($id = "", $days = null, $unitsFormat = "")
  1576.     {
  1577.         $id     = strtoupper($id);
  1578.         $status = $this->_checkLocationID($id);
  1579.  
  1580.         if (Services_Weather::isError($status)) {
  1581.             return $status;
  1582.         }
  1583.  
  1584.         // Get other data
  1585.         $units    = $this->getUnitsFormat($unitsFormat);
  1586.         $location = $this->getLocation($id);
  1587.  
  1588.         if ($this->_cacheEnabled && ($forecast = $this->_cache->get("METAR-".$id, "forecast"))) {
  1589.             // Wee... it was cached, let's have it...
  1590.             $forecastReturn  = $forecast;
  1591.             $this->_forecast = $forecastReturn;
  1592.             $forecastReturn["cache"] = "HIT";
  1593.         } else {
  1594.             // Set the source
  1595.             if ($this->_sourceTaf == "file") {
  1596.                 $source = realpath($this->_sourcePathTaf."/".$id.".TXT");
  1597.             } else {
  1598.                 $source = $this->_sourcePathTaf."/".$id.".TXT";
  1599.             }
  1600.  
  1601.             // Download and parse weather
  1602.             $forecastReturn  = $this->_parseForecastData($source);
  1603.  
  1604.             if (Services_Weather::isError($forecastReturn)) {
  1605.                 return $forecastReturn;
  1606.             }
  1607.             if ($this->_cacheEnabled) {
  1608.                 // Cache weather
  1609.                 $expire = constant("SERVICES_WEATHER_EXPIRES_FORECAST");
  1610.                 $this->_cache->extSave("METAR-".$id, $forecastReturn, $unitsFormat, $expire, "forecast");
  1611.             }
  1612.             $this->_forecast = $forecastReturn;
  1613.             $forecastReturn["cache"] = "MISS";
  1614.         }
  1615.  
  1616.         $this->_convertReturn($forecastReturn, $units, $location);
  1617.  
  1618.         return $forecastReturn;
  1619.     }
  1620.     // }}}
  1621. }
  1622. // }}}
  1623. ?>
  1624.